/*
 * Copyright (C) 2011 Mette Bank, Rikke Jensen, Kenneth Brodersen, Thomas Pedersen
 * 
 *     This file is part of digiPECS.
 * 
 *     digiPECS is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     digiPECS is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with digiPECS.  If not, see <http://www.gnu.org/licenses/>.
 *   
 * This file incorporates work covered by the following copyright and
 * permission notice:
 * 
 *     Copyright (C) 2008 The Android Open Source Project
 *
 *     Licensed under the Apache License, Version 2.0 (the "License");
 *     you may not use this file except in compliance with the License.
 *     You may obtain a copy of the License at
 *
 *          http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.
 */

package sw6.digipecs.widget.dragdrop;

import java.util.ArrayList;

import sw6.digipecs.exceptions.DraggableNotViewException;
import sw6.digipecs.exceptions.NoViewFoundException;
import sw6.digipecs.widget.BitmapImageView;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;

/**
 * This layout will handle drag and drop. The layout will construct a 
 * bitmap on top of the actual interface, which follows the touch. * 
 */
public class DragLayout extends FrameLayout {
	
	//TODO Draglayout should probably be using AnimationLayout for animations

	static private final int SPEED = 250;
	
	// Are we in drag mode ?
	private boolean mDragging = false;
	private boolean mDrawBitmap = false;
	
	// Location of the last motion seen
	private PointF mLastMotion = new PointF();

	// View and belonging bitmap to drag
	private View mView;
	private DraggableView mDraggableView;
	private Bitmap mDragBitmap;
	private Point mBitmapOffset = new Point();
	private Rect mRect = new Rect();
	private final static int RECT_OFFSET = 10;
	
	private Point mStartCoordinates = new Point();
	
	// Ignored drop target (e.g. the gallery, where objects cannot be dragged to)
	private ArrayList<View> mIgnoredDropTargets = new ArrayList<View>();
	
	// Last drop target touched
	private DropTarget mLastDropTarget;
	
	// Location of drop relative to view
	private int[] mDropCoordinates = new int[2];
	
	private ArrayList<DragListener> mListeners = new ArrayList<DragListener>();

	private Context mContext;

	private DragSource mDragSource;
	
	/**
	 * @constructor
	 * @param context - the context to work within.
	 */
	public DragLayout(Context context) {
		super(context);
		mContext = context;
	}
	/**
	 * Constructor - this constructor is need for the view to be inflated properly.
	 * @param context - the context to work within
	 * @param attrs - the collection of attributes, associated tags in XML document.
	 */
	 public DragLayout(Context context, AttributeSet attrs) {
		 super(context, attrs);
		 mContext = context;
	 }
	
	 /**
	  * Start dragging of a particular object
	  * @param dv - the view that is being dragged. 
	  */
	public void startDrag(DraggableView dv) {
		Log.v("sw6", "Starting drag..");
		
		if (!(dv instanceof View)) {
			throw new DraggableNotViewException();
		}
		
		// Create bitmap to drag
		// (Objects themselves are not dragged, instead "screenshots" of the object is dragged)
		final View view = (View) dv; 
		mView = view;
		mDraggableView = dv;
		view.buildDrawingCache(); // Drawing cache might need to be built before it can be used
		mDragBitmap = Bitmap.createBitmap(view.getDrawingCache());
		view.setVisibility(INVISIBLE);
		
		// Find drag source if it exists
		mDragSource = null;
		ViewParent vp = view.getParent();
		while (vp != null) {
			if (vp instanceof DragSource) {
				mDragSource = (DragSource) vp;
				break;
			}
			
			vp = vp.getParent();
		}
		
		mBitmapOffset.x = mDragBitmap.getWidth() / 2;
		mBitmapOffset.y = mDragBitmap.getHeight() / 2;
		
		// Find start coordinates
		int startX = mView.getLeft(); 
		int startY = mView.getTop();
		ViewParent vparent = mView.getParent();
		ViewGroup vgroup;
		while (vparent != null) {
			if (vparent instanceof ViewGroup) {
				vgroup = (ViewGroup) vparent;
				startX += vgroup.getLeft();
				startY += vgroup.getTop();
			}
			vparent = vparent.getParent();
		}
		startX += mBitmapOffset.x;
		startY += mBitmapOffset.y;
		mStartCoordinates.x = startX;
		mStartCoordinates.y = startY;
		
		dv.setDragging(true);
		mDragging  = true;
		mDrawBitmap = true;
		
		for (DragListener l : mListeners) l.onDragStart(view, dv);
		
		invalidate(); //Redraw the draglayout
	}
	/**
	 * Stop dragging a particular object
	 */
	public void stopDrag(){
		Log.v("sw6", "Stopping drag..");
		
		mDraggableView.setDragging(false);
		mLastDropTarget = null;
		
		for (DragListener l : mListeners) l.onDragStop();
		
		invalidate(); //Redraw the draglayout
	}
	/**
	 * Intercept touch events to children, get information of the event needed, and
	 * handle the touch event.
	 */
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {		
		// As we are dragging, we need to handle the event
		if (mDragging) {
			onTouchEvent(ev);
			return true;
		} else if(ev.getAction() == MotionEvent.ACTION_DOWN){
			// If we are not dragging and the action is down, save the coordinates
			mLastMotion.set(ev.getX(), ev.getY());
		}
		
		return super.onInterceptTouchEvent(ev);
	}

	/**
	 * Handle movement of drageble images.
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		
		// If we are not in drag mode this is not our event
		if (!mDragging) {
			return super.onTouchEvent(event);
		}
		
		// Handle the event, if we know how
		final int action = event.getAction();
		final float x = event.getX();
		final float y = event.getY();
		DropTarget droptarget;
		
		switch (action) {
		case MotionEvent.ACTION_MOVE:
			// Calculate top left corner of old bitmap
			// We assume that the highest coordinate value (bottom right corner) is not larger than MAX_VALUE of an integer, and therefore casting is safe to use
			final int dx = (int) mLastMotion.x - mBitmapOffset.x;
			final int dy = (int) mLastMotion.y - mBitmapOffset.y;
			
			// Invalidate old bitmap
			mRect.set(dx - RECT_OFFSET, dy - RECT_OFFSET, dx + mDragBitmap.getWidth() + RECT_OFFSET, dy + mDragBitmap.getHeight() + RECT_OFFSET);
			invalidate(mRect);
			
			// TODO Thomas: This might be cached by cheching if (x,y) is in mLastDropTaget first .. (WATCH OUT FOR USING THE RIGHT COORDINATES)
			try {
				final int[] dropCoordinates = mDropCoordinates;
				droptarget = findDropTarget(this, (int)x, (int)y, dropCoordinates);
				
				// Remove color from last drop target
				if (mLastDropTarget != null && droptarget != mLastDropTarget){
					mLastDropTarget.onDragOut();
				}
				
				// Change background colour on the target
				mLastDropTarget = droptarget;
				mLastDropTarget.onDragIn(dropCoordinates[0], dropCoordinates[1]);
			} catch (NoViewFoundException e) {
				if (mLastDropTarget != null) {
					mLastDropTarget.onDragOut();
					mLastDropTarget = null;
				}
			}
			
			// Set new last seen position
			mLastMotion.set(x, y);
			return true;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:
			// Check if any drop target is hit, and drop in it, and change background colour
			final int[] dropCoordinates = mDropCoordinates;
			
			try {
				droptarget = findDropTarget(this, (int)x, (int)y, dropCoordinates);
				if (!droptarget.drop(mView, dropCoordinates[0], dropCoordinates[1])) {
					rewindDrag();
				} else {
					mDragging = false;
					mDrawBitmap = false;
					if (mDragSource != null) mDragSource.onSuccessfulDrop(mView, droptarget);
				}
			} catch (NoViewFoundException e) {
				rewindDrag();
			}
			
			if(mLastDropTarget != null){
				mLastDropTarget.onDragOut();
			}
			
			stopDrag();
			
			return true;
		}
		
		// We don't know how to handle it, so let someone else do it
		return super.onTouchEvent(event);
	}
	
	private void rewindDrag() {
		mDrawBitmap = false;
		
		final BitmapImageView iv = new BitmapImageView(mContext, mDragBitmap);
		this.addView(iv);
		
		final int fromX = (int) (mLastMotion.x - mBitmapOffset.x);
		final int fromY = (int) (mLastMotion.y - mBitmapOffset.y);
		Animation animation = mDraggableView.getReturnAnimation(fromX, fromY, mDragBitmap);
		if (animation == null) {
			animation = new TranslateAnimation(fromX, mStartCoordinates.x - mBitmapOffset.x, fromY, mStartCoordinates.y - mBitmapOffset.y);
			animation.setDuration(SPEED);
		}
		
		final DragLayout dlayout = this;
		AnimationListener al = new AnimationListener() {
			@Override
			public void onAnimationStart(Animation animation) {
			}
				
			@Override
			public void onAnimationRepeat(Animation animation) {
			}
				
			@Override
			public void onAnimationEnd(Animation animation) {
				dlayout.removeView(iv);
				mDragging = false;
				mView.setVisibility(VISIBLE);
			}
		};
		animation.setAnimationListener(al);
		
		iv.startAnimation(animation);
	}

	// Draw the bitmap manually, just after all the children has draw themselves
	@Override
	protected void dispatchDraw(Canvas canvas) {
		// Draw children
		super.dispatchDraw(canvas);
		
		if (mDragging && mDrawBitmap) {
			final float x = mLastMotion.x - mBitmapOffset.x;
			final float y = mLastMotion.y - mBitmapOffset.x;
			synchronized (mDragBitmap) {
				if (!mDragBitmap.isRecycled()) canvas.drawBitmap(mDragBitmap, x, y, null);
			}
		}
	}	
	/**
	 * Set an view as ignored drop target. Will save a lot of
	 * searching the layout tree.
	 * @param dt - the view to be ignored as a drop target.
	 */
	public void addIgnoredDropTarget(View dt) {
		mIgnoredDropTargets.add(dt);
	}
	/*
	 * Find a drop target recursively (returns NULL if no target found) will
	 * otherwise return the first view that implements the droptarget interface.	 
	 */
	private DropTarget findDropTarget(ViewGroup container, int x, int y, int[] dropCoordinates) throws NoViewFoundException{
		final int count = container.getChildCount();
		
		//Check all children
		for(int i = count - 1; i >= 0; i--){
			final View child = container.getChildAt(i);
			
			// Check for visibility and ignored drop target
			if(child.getVisibility() == VISIBLE && !mIgnoredDropTargets.contains(child)){
				child.getHitRect(mRect);
				
				// Check if action is within child
				if(mRect.contains(x, y)){
					DropTarget target = null;
					
					x = x - child.getLeft();
					y = y - child.getTop();
					
					// If it is a ViewGroup, check its children, recursively
					if(child instanceof ViewGroup){
						try {
							target = findDropTarget((ViewGroup)child, x, y, dropCoordinates);
						} catch (NoViewFoundException e) {
							target = null;
						}
					}
					
					// If target not found, check if this child can be used
					if (target == null) {
						if (child instanceof DropTarget) {
							if (dropCoordinates != null) {
								dropCoordinates[0] = x;
								dropCoordinates[1] = y;
							}
							return (DropTarget)child;
						} else {
							throw new NoViewFoundException();
						}
					} else { // Else return the target already found
						return target;
					}					
				}
			}			
		}
		throw new NoViewFoundException();
	}
	
	public void addListener(DragListener listener) {
		mListeners.add(listener);
	}
	
}